前端编码

编码能力: 逻辑是否清晰、边界是否考虑到、思维是否活跃

手写 reduce

function MyReduce(fun, initValue) {
  if (this === null) {
    // this 不存在,抛出错误
    throw new TypeError(
      "Array.prototype.reduce " + "called on null or undefined"
    );
  }
  if (typeof fun !== "function") {
    // fun 不是function时,抛出错误
    throw new TypeError(fun + " is not a function");
  }
  const value = Object(this);
  let preValue, curValue, curIndex;
  // TODO: arr 初始值?
  if (initValue !== undefined) {
    preValue = initValue;
    curValue = arr[0];
    curIndex = 0;
  } else {
    preValue = arr[0];
    curValue = arr[1];
    curIndex = 1;
  }
  for (let i = curIndex; i < value.length; i++) {
    const item = value[i];
    preValue = fun(preValue, item, i, arr);
  }
  return preValue;
}
// 把方法写入到原型链上
Array.prototype.MyReduce = MyReduce;

实现一个算法,字符串包含"[]" , "()" , "{}",判断是否正确闭合

利用双指针加字典解法。

  1. 设置头尾双指针。
  2. 分别判断头尾两个指针对应的元素是否是闭合元素,如果不是闭合元素,头指针向右移动,尾指针向左移动,直到两个指针指向的都是闭合元素。
  3. 判断头尾对应的元素是不是闭合元素,不是则返回下标,重复 2 步骤。
var dir = {
  "{": "}",
  "[": "]",
  "(": ")",
  ")": "(",
  "}": "{",
};

function atWhereNoClose(str) {
  let len = str.length;
  // 双指针
  let left = 0;
  let right = len - 1;
  // 标记字符串是否闭合
  let flg = true;
  while (left < right) {
    // 未匹配左符号
    while (!/(\[|\{|\()/.test(str[left]) && left < right) {
      left++;
    }
    // 未匹配有符号
    while (!/(\}|\]|\))/.test(str[right]) && left < right) {
      right--;
    }

    // 如果左侧符号与右侧符号不是匹配的
    if (dir[str[left]] !== str[right]) {
      // 不匹配的情况下,返回未闭合符号的下标
      if (dir[str[left]] && left <= right) {
        flg = left;
        break;
      }
    } else {
      // 如果匹配则移动两个指针
      left++;
      right--;
    }
  }
  return flg;
}
console.log(atWhereNoClose("{[[ab}}"));
console.log(atWhereNoClose("{ab}"));
console.log(atWhereNoClose("{({[ab]})}"));
console.log(atWhereNoClose("ab}"));

JS 打乱数组

function getArrRandomly(arr) {
  var len = arr.length;
  for (var i = 0; i < len; i++) {
    var randomIndex = Math.floor(Math.random() * (len - i)); // 这里一定要注意,后面不管是(i+1)还是(len-i),它们是时变的。
    // 随机下标字符与 i 位字符进行交接
    var itemAtIndex = arr[randomIndex];
    arr[randomIndex] = arr[i];
    arr[i] = itemAtIndex;
  }
  return arr;
}

懒加载实现

let lazyImages = [...document.querySelectorAll(".lazy-image")];
let inAdvance = 300; // 自定义一个高度,当距离 300px 到达图片时加载

function lazyLoad() {
  lazyImages.forEach((image) => {
    if (image.offsetTop < window.innerHeight + window.pageYOffset + inAdvance) {
      // 从预设值中获取真实的图片 url.
      image.src = image.dataset.src;
      image.onload = () => image.classList.add("loaded");
    }
  });
}

lazyLoad();
window.addEventListener("scroll", _.debounce(lazyLoad, 16)); // 用到了 lodash 的防抖函数.
window.addEventListener("resize", _.debounce(lazyLoad, 16));

JS 发布订阅模式

const event = {
  clientList: [],
  listen: function (key, fn) {
    if (this.clientListen[key]) {
      this.clientList[key] = [];
    }
    this.clientList[key].push(fn);
  },
  trigger: function () {
    const key = Array.prototype.shift.call(arguments);
    const fns = this.clientList[key];
    if (!fns || fns.length === 0) {
      return false;
    }
    for (let i = 0, fn; (fn = fns[i++]); ) {
      fn.apply(this, arguments);
    }
  },
  remove: function (key, fn) {
    const fns = this.clientList[key];
    if (!fns) {
      return false;
    }
    if (!fn) {
      fns && (fns.length = 0);
    } else {
      for (let l = fns.length - 1; l >= 0; l--) {
        const _fn = fns[l];
        if (_fn === fn) {
          fns.splice(l, 1);
        }
      }
    }
  },
};

深拷贝

1. JSON.parse JSON.stringify

简单的做法:JSON.parse(JSON.stringify(obj)), 但是该方法也是有局限性的:

  1. 会忽略undefined, symbol, 函数
  2. 不能解决循环引用的对象 (会报错)

2. MessageChannel

如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel。 这种方法有局限性:

  1. 注意该方法是异步的
  2. 当属性值是函数的时候,会报错
function structuralClone(obj) {
  return new Promise((resolve) => {
    const { port1, port2 } = new MessageChannel();
    port2.onmessage = (ev) => resolve(ev.data);
    port1.postMessage(obj);
  });
}

var obj = {
  a: 1,
  b: {
    c: 2,
  },
};

obj.b.d = obj.b;

// 可以处理 undefined 和循环引用对象
const test = async () => {
  const clone = await structuralClone(obj);
  console.log(clone);
};
test();

3. for 循环深拷贝

// 判断属性值类型是原始类型和引用类型
function isObj(obj) {
  return typeof obj === "object" && obj !== null;
}

// 为什么要用 WeakMap, key 可以使用对象? 更安全?
function deepClone(obj, map = new WeakMap()) {
  // 解决环的情况
  if (map.has(obj)) return map.get(obj);
  // 数组 or 对象
  let cloneObj = Array.isArray(obj) ? [] : {};
  map.set(obj, cloneObj);
  // for-in 会遍历原型链上的属性
  for (let key in obj) {
    // 需要判断是否是原型链上的属性,不是原型链才拷贝
    if (obj.hasOwnProperty(key)) {
      // 原始类型直接赋值(注意 null)
      cloneObj[key] = isObj(obj[key]) ? deepClone(obj[key], map) : obj[key];
    }
  }
  return cloneObj;
}

手写 call、apply 及 bind 函数

Function.prototype.myCall = function (context) {
  context = context || window;
  // 通过将函数挂载到 context 对象上来使得 this 改变
  context.fn = this;
  const args = [...arguments].slice(1);
  const result = context.fn(...args);
  delete context.fn;
  return result;
};
Function.prototype.myApply = function (context) {
  context = context || window;
  context.fn = this;
  let result;
  // 处理参数和 call 有区别
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  delete context.fn;
  return result;
};

bind 实现

bind 的实现对比其他两个函数略微地复杂了一点,因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现

Function.prototype.myBind = function (context) {
  if (typeof this !== "function") {
    throw new TypeError(
      "Function.prototype.bind - what is trying to be bound is not callable"
    );
  }
  const that = this;
  const args = [...arguments].slice(1);
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    // 通过 `new` 的方式,对于 `new` 的情况来说,不会被任何方式改变 `this`,所以对于这种情况我们需要忽略传入的 `this`
    if (this instanceof F) {
      return new that(...args, ...arguments);
    }
    return that.apply(context, args.concat(...arguments));
  };
};
Function.prototype.bind = function () {
  var self = this, // 保存原函数
    context = [].shift.call(arguments), // 保存需要绑定的 this 上下文
    args = [].slice.call(arguments); // 剩余的参数转为数组

  return function () {
    // 返回一个新函数
    self.apply(context, [].concat.call(args, [].slice.call(arguments)));
  };
};

数组去重(对象、非对象)

// set 去重
[...new Set(arr)];

// 利用 indexOf 去重
const newArr = arr.filter((item, index, arr) => index === arr.indexOf(item));

// 利用对象去重
let objA = {};
const newArrA = arr.filter((item, index, arr) =>
  objA.hasOwnProperty(item) ? false : (objA[item] = true)
);

数组对象去重

根据对象的某一个属性去重

function removeDuplication(arr = [], key = "") {
  if (!key) return Array.from(new Set(arr));
  const map = {};
  return arr.reduce((result, next) => {
    map[next[key]] ? "" : (map[next[key]] = true && result.push(next));
    return result;
  }, []);
}

实现 flatten 扁平化函数

编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组。

竟然原生就有这个 flat 函数,用来拍平数组。flat 函数的参数是层级。Infinity 无限大。 会拍平数组中的所有数组值。

Array.from(new Set(arr.flat(Infinity))).sort((a, b) => a - b);

自己实现:

const flatten = (arr) =>
  arr.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);

递归实现

function flatten(arr, result = []) {
  arr.forEach((item) => {
    if (Array.isArray(item)) {
      flatten(item, result);
    } else {
      result.push(item);
    }
  });
  return result;
}

有层级的数组扁平化

function flatten(arr, deep) {
  if (!deep) return arr;
  let temp = [];
  arr.forEach((item) => {
    if (Array.isArray(item)) {
      temp.push(...flatten(item, --deep));
    } else {
      temp.push(item);
    }
  });
  return temp;
}

给出数组超过半数的数字,不存在的话输出没有(要求时间复杂度最低)

数组中有一个数字出现的次数超过数组长度的一半,说明它出现的次数比其他所有数字出现次数的和还要多

function moreThanHalfNum(numbers) {
  var obj = {};
  var len = numbers.length;
  // 统计字符出现的次数
  numbers.forEach(function (s) {
    if (obj[s]) {
      obj[s]++;
    } else {
      obj[s] = 1;
    }
  });

  for (var i in obj) {
    if (obj[i] > Math.floor(len / 2)) {
      return i;
    }
  }
  return 0;
}

实现一下 curry(柯里化)

柯里化好处:

  1. 保留上一步的传参,能够进行延迟计算
  2. 优雅的写法,允许你写出来的代码更干净、更有表达力。
function curry(fn, arr = []) {
  return fn.length === arr.length
    ? fn.apply(null, arr)
    : function (...args) {
        return curry(fn, arr.concat(args));
      };
}

const curry = (fn, arr = []) =>
  fn.length === arr.length
    ? fn(...arr)
    : (...args) => curry(fn, [...arr, ...args]);

add 柯里化函数

用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数

add(2, 5); // 7
add(2)(5); // 7

实现

function add() {
  var args = Array.prototype.slice.call(arguments);
  // 返回一个函数,需要可以继续执行。
  var fn = function () {
    // 保存上一次传入的所有参数
    var fn_args = Array.prototype.slice.call(arguments);
    return add.apply(null, args.concat(fn_args));
  };

  // 返回指定对象的原始值。 不加这个的话,最终返回的是一个函数,无法最终拿到结果
  fn.valueOf = function () {
    return args.reduce(function (a, b) {
      return a + b;
    });
  };
  return fn;
}

实现一个 EventEmitter 类

function EventEmitter() {
  this.callbacksMap = {};
}

EventEmitter.prototype.on = function (type, handler) {
  const callbacks = this.callbacksMap[type];
  if (!callbacks) {
    this.callbacksMap[type] = [handler];
  } else {
    callbacks.push(handler);
  }
  return this;
};

EventEmitter.prototype.off = function (type, handler) {
  const list = this.callbacksMap[type] || [];

  for (let i = list.length; i >= 0; --i) {
    if (!handler || list[i] === handler) {
      list.splice(i, 1);
    }
  }

  return this;
};

EventEmitter.prototype.emit = function (type, data) {
  const list = this.callbacksMap[type];

  if (list) {
    for (let i = 0, len = list.length; i < len; ++i) {
      list[i].call(this, data);
    }
  }
};

EventEmitter.prototype.once = function (type, handler) {
  const self = this;

  function wrapper() {
    handler.apply(self, arguments);
    self.off(type, wrapper);
  }
  this.on(type, wrapper);
  return this;
};

解析一个参数

function getQueryString(name) {
  const reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
  const r = window.location.search.slice(1).match(reg);
  if (r != null) {
    return r[2];
  }
  return null;
}

url 所有参数转化为一个对象

var parseQueryString = function (search) {
  if (!search) return {};
  const regExp = /([^&=]+)=([\w\W]*?)(&|$)/g;
  const ret = {};
  search = search[0] === "?" ? search.slice(1) : search;
  while ((result = regExp.exec(search)) != null) {
    ret[result[1]] = result[2];
  }
  return ret;
};

手写原生 ajax

手写的 ajax 是否兼容 IE , IE 下面的 ajax 与普通浏览器的 ajax 对象不一样

function ajax(url, cb) {
  let xhr;
  // 创建 XMLHttpRequest 对象
  if (window.XMLHttpRequest) {
    // `XMLHttpRequest`只有在高级浏览器中才支持. 非 IE 内核
    xhr = new XMLHttpRequest();
  } else {
    // IE内核
    xhr = ActiveXObject("Microsoft.XMLHTTP");
  }
  // 绑定 onreadystatechange 事件
  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4 && xhr.status == 200) {
      //  获取异步调用返回的数据
      cb(xhr.responseText);
    }
  };
  // 向服务器发送请求. 第三个参数表示是否异步
  xhr.open("GET", url, true);
  xhr.send();
}

实现 destructuringArray 方法

destructuringArray([1,[2,4],3], "[a,[b],c]");
result: { a:1, b:2, c:3 }
const destructuringArray = (value, keys) => {
  const obj = {};
  // "[a,[b],c]".replace(/\w+/g, '"$&"'). 能够将 keys 变成字符串数组
  const arr = JSON.parse(keys.replace(/\w+/g, '"$&"'));
  const iterate = (value, keys) => {
    keys.forEach((item, index) => {
      if (Array.isArray(item)) iterate(value[index], item);
      else obj[item] = value[index];
    });
  };
  iterate(value, arr);
  return obj;
};

reduce 实现 map

Array.prototype.map = function (fn) {
  const arr = this;
  return arr.reduce((acc, cur, i) => {
    acc.push(fn(cur, i, arr));
    return acc;
  }, []);
};

千分位

如 12000000.11 转化为 12,000,000.11

function test1(num) {
  var str = +num + "";
  var len = str.length;
  if (len <= 3) return str;
  num = "";
  while (len > 3) {
    len -= 3;
    num = "," + str.substr(len, 3) + num;
  }
  return str.substr(0, len) + num;
}

function test2(num) {
  // ?= 正向匹配:匹配位置
  // ?! 正向不匹配:排除位置
  var str = (+num).toString();
  var reg = /(?=(?!\b)(\d{3})+$)/g;
  return str.replace(reg, ",");
}
function commafy(num) {
  // 取绝对值
  const val = Math.abs(num);
  // 取正负值
  const isPositive = num === val;
  // 正则表达式 ?= 前瞻:exp1(?=exp2) 查找exp2前面的exp1
  // 正则表达式 ?! 负前瞻:exp1(?=exp2) exp1(?!exp2) 查找后面不是exp2的exp1
  const result =
    val &&
    val.toString().replace(/(\d)(?=(\d{3})+\.)/g, function ($1, $2) {
      return $2 + ",";
    });
  return isPositive ? result : `-${result}`;
}
function format(str) {
  return str.replace(/(\d)(?=(?:\d{3})+$)/g, "$1,");
}

a~z 有 26 个字母,按照 1~26 编码,现在给定一个数字字符串,输出所有可能的解码结果,如:输入 1234,输出 ['axd', 'abcd', 'lcd']

/**
 * @param {string} s
 * @return {number}
 */
var numDecodings = function (s) {
  if (s.charAt(0) == "0") return;

  const chars = s.split();
  return decode(chars, chars.length - 1);
};

function decode(chars, index) {
  //  处理到了第一个字符,只能有一种解码方法,返回  1
  if (index <= 0) return 1;

  let count = 0;

  const curr = chars[index];
  const prev = chars[index - 1];

  //  当前字符比  “0”  大,则直接利用它之前的字符串所求得的结果
  if (curr > "0") {
    count = decode(chars, index - 1);
  }

  //  由前一个字符和当前字符所构成的数字,值必须要在  1  到  26  之间,否则无法进行解码
  if (prev == "1" || (prev == "2" && curr <= "6")) {
    count += decode(chars, index - 2);
  }

  return count;
}

写一个大数相乘的解决方案。传两个字符串进来,返回一个字符串

function isAN(number) {
  return /^d+$/.test(number);
}
/**
 * @param {string} num1
 * @param {string} num2
 * @return {string}
 */
let multiply = function (num1 = "", num2 = "") {
  //判断输入是不是数字
  if (isAN(num1) || isAN(num2)) return "";
  let len1 = num1.length,
    len2 = num2.length;
  let ans = [];

  //这里倒过来遍历很妙,不需要处理进位了
  for (let i = len1 - 1; i >= 0; i--) {
    for (let j = len2 - 1; j >= 0; j--) {
      let index1 = i + j,
        index2 = i + j + 1;
      let mul = num1[i] * num2[j] + (ans[index2] || 0);
      // 当前位
      ans[index1] = Math.floor(mul / 10) + (ans[index1] || 0);
      // 进位
      ans[index2] = mul % 10;
    }
  }

  //去掉前置 0
  let result = ans.join("").replace(/^0+/, "");

  //不要转成数字判断,否则可能会超精度!
  return !result ? "0" : result;
};

大数相加

1000000000 + 1000000000 允许返回字符串。处理大数。大数问题其实就是通过字符串来处理,从后往前加,然后处理进位的问题。

function add(a, b) {
  // 取两个数字的最大长度
  let maxLength = Math.max(a.length, b.length);
  // 用 0 去补齐长度
  a = a.padStart(maxLength, 0); //"0009007199254740991"
  b = b.padStart(maxLength, 0); //"1234567899999999999"

  // 定义加法过程中需要用到的变量
  let t = 0;
  let f = 0; // 进位
  let sum = "";

  for (let i = maxLength - 1; i >= 0; i--) {
    t = parseInt(a[i]) + parseInt(b[i]) + f;
    // Math.floor 向下取整。 Math.ceil 向上取整。
    f = Math.floor(t / 10); // 获取需要进位的值
    sum = (t % 10) + sum; // 余数是当前为数的值
  }

  // 最后一位的进位
  if (f == 1) {
    sum = "1" + sum;
  }

  return sum;
}

加法函数

写一个处理加法可能产生精度的函数,比如 0.1 + 0.2 = 0.3

function accAdd(arg1, arg2) {
  var r1, r2, m;
  try {
    r1 = arg1.toString().split(".")[1].length;
  } catch (e) {
    r1 = 0;
  }
  try {
    r2 = arg2.toString().split(".")[1].length;
  } catch (e) {
    r2 = 0;
  }
  // 计算出一个倍数。看小数点后面的值的长度
  m = Math.pow(10, Math.max(r1, r2));
  // 先乘这个倍数,再除以这个倍数
  return (arg1 * m + arg2 * m) / m;
}
var result = accAdd(0.1, 0.2);
console.log(result); // 0.3

自定义处理函数,放大指定的位数,最后再缩小。

// f代表需要计算的表达式,digit代表小数位数
Math.formatFloat = function (f, digit) {
  // Math.pow(指数,幂指数)
  var m = Math.pow(10, digit);
  // Math.round() 四舍五入
  return Math.round(f * m, 10) / m;
};
console.log(Math.formatFloat(0.3 * 8, 1)); // 2.4
console.log(Math.formatFloat(0.35 * 8, 2)); // 2.8

js 浮点数运算不精确如何解决?

实现一个判断变量类型的函数

function getType(obj) {
  // 分开判断引用类型和基本类型
  return typeof obj === "object"
    ? Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
    : typeof obj;
}

二进制相加,给两字符串求值

对输入的字符串:去除其中的字符'b';去除相邻的'a'和'c'。 'aabcd' -> 'ad' 'aaabbccc' -> '' 不允许使用类似 string.replace 函数。要求时间、空间复杂度尽量优化

js 中处理大数。如果后端传给前端一个很大的数,前端会怎么样,该怎么处理?

找出两个有序数组中的重复项,分析时间和空间复杂度

工厂生产了十批原件,其中有一批是次品。正常的原件每个重量都是 100 克,次品的是 99 克。现在给你一个秤,要求只称一次,怎么确定哪批原件是次品。

比较版本号

var compareVersion = function (version1, version2) {
  const arr1 = version1.split(".");
  const arr2 = version2.split(".");

  let len = Math.max(arr1.length, arr2.length);

  for (let i = 0; i < len; i++) {
    let data1 = 0,
      data2 = 0;

    if (i < arr1.length) {
      data1 = parseInt(arr1[i]);
    }
    if (i < arr2.length) {
      data2 = parseInt(arr2[i]);
    }

    if (data1 < data2) {
      return -1;
    } else if (data1 > data2) {
      return 1;
    }
  }
  return 0;
};

手写 JSONP

jsonp 原理:因为 jsonp 发送的并不是 ajax 请求,其实是动态创建 script 标签 script 标签是没有同源限制的,把 script 标签的 src 指向请求的服务端地址。

function jsonp(url, data = {}, callback = "callback") {
  //处理json对象,拼接url
  data.callback = callback;
  let params = [];
  for (let key in data) {
    params.push(key + "=" + data[key]);
  }
  let script = document.createElement("script");
  script.src = url + "?" + params.join("&");
  document.body.appendChild(script);

  //返回Promise
  return new Promise((resolve, reject) => {
    window[callback] = (data) => {
      try {
        resolve(data);
      } catch (e) {
        reject(e);
      } finally {
        //移除 script 元素
        script.parentNode.removeChild(script);
        console.log(script);
      }
    };
  });
}

//请求数据
jsonp(
  "http://photo.sina.cn/aj/index",
  {
    page: 1,
    cate: "recommend",
  },
  "jsonCallback"
).then((data) => {
  console.log(data);
});

Jsonp 方案需要服务端怎么配合 ?

用两个栈实现队列

用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。

思路

一个栈用来存储插入队列数据,一个栈用来从队列中取出数据。从第一个栈向第二个栈转移数据的过程中:数据的性质已经从后入先出变成了先入先出。

var CQueue = function () {
  this.outStack = [];
  this.inStack = [];
};

/**
 * @param {number} value
 * @return {void}
 */
CQueue.prototype.appendTail = function (value) {
  if (value) {
    // 新插入队列的数据都放在 inStack
    this.inStack.push(value);
  }
};

/**
 * @return {number}
 */
CQueue.prototype.deleteHead = function () {
  const { outStack, inStack } = this;
  //  如果 outStack 为空,那么将 inStack 中的元素都转移过来
  if (!outStack.length) {
    while (inStack.length) {
      outStack.push(inStack.pop());
    }
  }
  return !outStack.length ? -1 : outStack.pop();
};

js 实现一个拖拽?

首先是三个事件,分别是 mousedown,mousemove,mouseup 当鼠标点击按下的时候,需要一个 tag 标识此时已经按下,可以执行 mousemove 里面的具体方法。

clientX,clientY 标识的是鼠标的坐标,分别标识横坐标和纵坐标,并且我们用 offsetX 和 offsetY 来表示元素的元素的初始坐标,移动的举例应该是: 鼠标移动时候的坐标-鼠标按下去时候的坐标。

也就是说定位信息为:

鼠标移动时候的坐标-鼠标按下去时候的坐标+元素初始情况下的 offetLeft.

还有一点也是原理性的东西,也就是拖拽的同时是绝对定位,我们改变的是绝对定位条件下的 left 以及 top 等等值。 div:

<div class="drag" style="left:0;top:0;width:100px;height:100px">按住拖动</div>

<style>
  .drag {
    background-color: skyblue;
    position: absolute;
    line-height: 100px;
    text-align: center;
  }
</style>

js:

// 获取DOM元素
let dragDiv = document.getElementsByClassName("drag")[0];
// 鼠标按下事件 处理程序
let putDown = function (event) {
  dragDiv.style.cursor = "pointer";
  let offsetX = parseInt(dragDiv.style.left); // 获取当前的x轴距离
  let offsetY = parseInt(dragDiv.style.top); // 获取当前的y轴距离
  let innerX = event.clientX - offsetX; // 获取鼠标在方块内的x轴距
  let innerY = event.clientY - offsetY; // 获取鼠标在方块内的y轴距
  // 按住鼠标时为div添加一个border
  dragDiv.style.borderStyle = "solid";
  dragDiv.style.borderColor = "red";
  dragDiv.style.borderWidth = "3px";
  // 鼠标移动的时候不停的修改div的left和top值
  document.onmousemove = function (event) {
    dragDiv.style.left = event.clientX - innerX + "px";
    dragDiv.style.top = event.clientY - innerY + "px";
    // 边界判断
    if (parseInt(dragDiv.style.left) <= 0) {
      dragDiv.style.left = "0px";
    }
    if (parseInt(dragDiv.style.top) <= 0) {
      dragDiv.style.top = "0px";
    }
    if (
      parseInt(dragDiv.style.left) >=
      window.innerWidth - parseInt(dragDiv.style.width)
    ) {
      dragDiv.style.left =
        window.innerWidth - parseInt(dragDiv.style.width) + "px";
    }
    if (
      parseInt(dragDiv.style.top) >=
      window.innerHeight - parseInt(dragDiv.style.height)
    ) {
      dragDiv.style.top =
        window.innerHeight - parseInt(dragDiv.style.height) + "px";
    }
  };
  // 鼠标抬起时,清除绑定在文档上的mousemove和mouseup事件
  // 否则鼠标抬起后还可以继续拖拽方块
  document.onmouseup = function () {
    document.onmousemove = null;
    document.onmouseup = null;
    // 清除border
    dragDiv.style.borderStyle = "";
    dragDiv.style.borderColor = "";
    dragDiv.style.borderWidth = "";
  };
};
// 绑定鼠标按下事件
dragDiv.addEventListener("mousedown", putDown, false);

需要通过 threshold 参数控制调用函数频率

const yourFunction = function (func, threshold) {
  // 请实现
};
const triggerSearch = yourFunction((val) => {
  const { onSearch } = this.props;
  onSearch(val);
}, 300);
triggerSearch(searchText);

实现

const yourFunction = function (func, threshold) {
  let timeOut;
  return function () {
    if (!timeOut) {
      timeOut = setTimeout(() => {
        timeOut = null;
        func.apply(this, arguments);
      }, threshold);
    }
  };
};

const triggerSearch = yourFunction((val) => {
  const { onSearch } = this.props;
  onSearch(val);
}, 300);

事件触发器

兼容所有浏览器

var fireEvent = function (element, event) {
  if (document.createEventObject) {
    var mockEvent = document.createEventObject();
    return element.fireEvent("on" + event, mockEvent);
  } else {
    var mockEvent = document.createEvent("HTMLEvents");
    mockEvent.initEvent(event, true, true);
    return element.dispatchEvent(mockEvent);
  }
};

代码中 a 在什么情况下会打印 1?

var a = ?;
if(a == 1 && a == 2 && a == 3){
 	console.log(1);
}

考察隐式转换,重写 toString 方法即可

var a = {
  i: 1,
  toString() {
    return a.i++;
  },
};

if (a == 1 && a == 2 && a == 3) {
  console.log(1);
}
let a = {
  i: 1,
  valueOf() {
    return a.i++;
  },
};

if (a == 1 && a == 2 && a == 3) {
  console.log("1");
}
var a = [1, 2, 3];
a.join = a.shift;
if (a == 1 && a == 2 && a == 3) {
  console.log("1");
}
let a = {
  [Symbol.toPrimitive]: (
    (i) => () =>
      ++i
  )(0),
};
if (a == 1 && a == 2 && a == 3) {
  console.log("1");
}

实现 (5).add(3).minus(2) 功能

Number.prototype.add = function (value) {
  let number = parseFloat(value);
  if (typeof number !== "number" || Number.isNaN(number)) {
    throw new Error("请输入数字或者数字字符串~");
  }
  return this + number;
};
Number.prototype.minus = function (value) {
  let number = parseFloat(value);
  if (typeof number !== "number" || Number.isNaN(number)) {
    throw new Error("请输入数字或者数字字符串~");
  }
  return this - number;
};

要求设计 LazyMan 类,实现以下功能

LazyMan("Tony");
// Hi I am Tony

LazyMan("Tony").sleep(10).eat("lunch");
// Hi I am Tony
// 等待了10秒...
// I am eating lunch

LazyMan("Tony").eat("lunch").sleep(10).eat("dinner");
// Hi I am Tony
// I am eating lunch
// 等待了10秒...
// I am eating diner

LazyMan("Tony")
  .eat("lunch")
  .eat("dinner")
  .sleepFirst(5)
  .sleep(10)
  .eat("junk food");
// Hi I am Tony
// 等待了5秒...
// I am eating lunch
// I am eating dinner
// 等待了10秒...
// I am eating junk food

答案:

class LazyManClass {
  constructor(name) {
    this.taskList = [];
    this.name = name;
    console.log(`Hi I am ${this.name}`);
    setTimeout(() => {
      this.next();
    }, 0);
  }
  eat(name) {
    var that = this;
    var fn = (function (n) {
      return function () {
        console.log(`I am eating ${n}`);
        that.next();
      };
    })(name);
    this.taskList.push(fn);
    return this;
  }
  sleepFirst(time) {
    var that = this;
    var fn = (function (t) {
      return function () {
        setTimeout(() => {
          console.log(`等待了${t}秒...`);
          that.next();
        }, t * 1000);
      };
    })(time);
    this.taskList.unshift(fn);
    return this;
  }
  sleep(time) {
    var that = this;
    var fn = (function (t) {
      return function () {
        setTimeout(() => {
          console.log(`等待了${t}秒...`);
          that.next();
        }, t * 1000);
      };
    })(time);
    this.taskList.push(fn);
    return this;
  }
  next() {
    var fn = this.taskList.shift();
    fn && fn();
  }
}
function LazyMan(name) {
  return new LazyManClass(name);
}
LazyMan("Tony")
  .eat("lunch")
  .eat("dinner")
  .sleepFirst(5)
  .sleep(4)
  .eat("junk food");

实现函数字符串转对象

'a.b.c'
=>
a: {
    b: {
        c: null
    }
}

输入字符串输出二维数组

`
12312

1  3
12 3
`
=>
[
    ['12312'],
    ['13'],
    ['123']
]
function handler(str = "") {
  return str
    .split("\n")
    .filter(Boolean)
    .map((val) => val.replace(/\s/g, ""));
}

实现 toFix 函数

atoi 把任意进制的数转为十进制的数。 需要考虑负数。

十六进制转十进制

// 10进制转16进制
function handler(params) {
  typeof params == "string" ? (params = Number(params)) : "";
  console.log(params.toString(16)); //a8
}
handler("168");

// 16进制转10进制
function handler2(params) {
  console.log(Number("0x" + params)); //168
}
handler2("a8");

// 16进制转10进制
function handler3(params) {
  console.log(parseInt(params, 16)); //168
}
handler3("a8");

实现 36 进制转换

function getNums36() {
  var nums36 = [];
  for (var i = 0; i < 36; i++) {
    if (i >= 0 && i <= 9) {
      nums36.push(i);
    } else {
      nums36.push(String.fromCharCode(i + 87));
    }
  }
  return nums36;
}

//十进制数转成36进制
function scale36(n) {
  var arr = [];
  var nums36 = getNums36();
  while (n) {
    var res = n % 36;
    //作为下标,对应的36进制数,转换成
    arr.unshift(nums36[res]);
    //去掉个位
    n = parseInt(n / 36);
  }
  return arr.join("");
}

解析对象属性

function find(obj, str) {
  var arr = str.split("."); // 将传入的字符串按照"."切割成数组
  var pointer = obj; // 初始化指针变量为传入的对象本身
  for (var i = 0; i < arr.length; i++) {
    var key = arr[i];
    if (pointer.hasOwnProperty(key)) {
      // 判断当前指针变量所指的对象中是否包含该属性
      pointer = pointer[key]; // 将指针变量指向该属性对应的对象
    } else {
      return undefined; // 如果不存在该属性,返回undefined
    }
  }
  return pointer; // 循环结束时,指针变量所指的对象即为所要查找的属性值
}

// 示例
var obj = { a: { b: { c: 1 } } };
console.log(find(obj, "a.b.c")); // 1
console.log(find(obj, "a.d.c")); // undefined

实现一个定时器函数 myTimer(fn, a, b)

实现一个定时器函数 myTimer(fn, a, b), 让 fn 执行, 第一次执行是 a 毫秒后, 第二次执行是 a+b 毫秒后, 第三次是 a+2b 毫秒, 第 N 次执行是 a+Nb 毫秒后

要求: 1、白板手撕 2、myTimer 要有返回值,并且返回值是一个函数,调用该函数,可以让 myTimer 停掉

function myTimer(fn, a, b) {
  let timerId;
  let count = 0;

  function schedule() {
    const delay = a + count * b;
    timerId = setTimeout(() => {
      fn();
      count++;
      schedule();
    }, delay);
  }

  schedule();

  return function () {
    clearTimeout(timerId);
  };
}

实现一个持续的动画效果

js 定时器实现

var e = document.getElementById("e");
var flag = true;
var left = 0;
setInterval(() => {
  left == 0 ? (flag = true) : left == 100 ? (flag = false) : "";
  flag ? (e.style.left = ` ${left++}px`) : (e.style.left = ` ${left--}px`);
}, 1000 / 60);

js API requestAnimationFrame 实现

//兼容性处理
window.requestAnimFrame = (function () {
  return (
    window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    function (callback) {
      window.setTimeout(callback, 1000 / 60);
    }
  );
})();

var e = document.getElementById("e");
var flag = true;
var left = 0;

function render() {
  left == 0 ? (flag = true) : left == 100 ? (flag = false) : "";
  flag ? (e.style.left = ` ${left++}px`) : (e.style.left = ` ${left--}px`);
}

(function animloop() {
  render();
  requestAnimFrame(animloop);
})();

优势:

浏览器可以优化并行的动画动作,更合理的重新排列动作序列,并把能够合并的动作放在一个渲染周期内完成,从而呈现出更流畅的动画效果 解决毫秒的不精确性 避免过度渲染(渲染频率太高、tab 不可见暂停等等) 注:requestAnimFrame 和 定时器一样也头一个类似的清除方法 cancelAnimationFrame

css 实现:

.test {
  animation: mymove 5s infinite;
  @keyframes mymove {
    from {
      top: 0px;
    }
    to {
      top: 200px;
    }
  }
}

原生 js 实现 MVVM

<span id="box">
  <h1 id="text"></h1>
  <input type="text" id="input" oninput="inputChange(event)" />
  <button id="button" onclick="clickChange()">
    Click me
  </button>
</span>
const input = document.getElementById("input");
const text = document.getElementById("text");
const button = document.getElementById("button");
const data = {
  value: "",
};
function defineProperty(obj, attr) {
  let val;
  Object.defineProperty(obj, attr, {
    set(newValue) {
      console.log("set");
      if (val === newValue) {
        return;
      }
      val = newValue;
      input.value = newValue;
      text.innerHTML = newValue;
    },
    get() {
      console.log("get");
      return val;
    },
  });
}
defineProperty(data, "value");
function inputChange(event) {
  data.value = event.target.value;
}

function clickChange() {
  data.value = "hello";
}

实现模板字符串解析

描述:实现函数使得将 template 字符串中的 {{}} 内的变量替换。

核心:使用字符串替换方法 str.replace(regexp|substr, newSubStr|function),使用正则匹配代换字符串。

实现:

function render(template, data) {
  // 模板字符串正则 /\{\{(\w+)\}\}/, 加 g 为全局匹配模式, 每次匹配都会调用后面的函数
  let computed = template.replace(/\{\{(\w+)\}\}/g, function (match, key) {
    // match: 匹配的子串;  key:括号匹配的字符串
    return data[key];
  });
  return computed;
}

// 测试
let template = "我是{{name}},年龄{{age}},性别{{sex}}";
let data = {
  name: "张三",
  age: 18,
};
console.log(render(template, data)); // 我是张三,年龄18,性别undefined

实现一个构造函数,new 的时候每次加 1

new 操作符干了什么

找出字符串中出现次数最多的那一个字符

// 1、转换成键值格式数据 eg.'程序员程序员' -> {'程': 2, '序': 2, '员': 2}
// 2、再转换成数组格式 eg.{'程': 2, '序': 2, '员': 2}  ->[2]:['程', '序', '员']。
function getMostChar2(str) {
  var strArr = str.split(""),
    obj = {},
    arr = [],
    len = strArr.length,
    i,
    key;

  for (i = 0; i < len; i++) {
    obj[strArr[i]] ? obj[strArr[i]]++ : (obj[strArr[i]] = 1); //记录数目
  }

  for (key in obj) {
    arr[obj[key]] ? arr[obj[key]].push(key) : (arr[obj[key]] = [key]); //取出
  }

  return { mostChar: arr[arr.length - 1].join(), maxLen: arr.length - 1 };
}

请写一个将字符串转成驼峰的方法?

例如:border-bottom-color -> borderBottomColor

function camelCase(str) {
  return (
    str &&
    str.replace(/-([a-z]|[0-9])/gi, function (all, $1) {
      return ($1 + "").toUpperCase();
    })
  );
}

数字转换中文大写

toChineseNum(12345); // 一万二千三百四十五

const toChineseNum = (num) => {
  const keys = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
  const count = ["", "十", "百", "千"];
  var str = "",
    nums = num.toString().split("").reverse();

  nums.map(function (value, index) {
    str =
      keys[value] +
      (value == 0 ? "" : count[index > 3 ? index % 4 : index]) +
      (index == 4 ? "万" : "") +
      str;
  });

  /*
   * 需要去掉的零:
   * 1.后面跟着零的零
   * 2.最后连续的零
   * 3.万前面的零
   */
  return str.replace(/零(?=零)|零$|零(?=万)/g, "");
};

js 去除字符串空格

去除所有空格

str.replace(/\s/g, "");

去除两边空格

str.replace(/^\s+|\s+\$/g, "");

原生方法

str.trim();

怎么去除字符串紧邻相同的字符。比如:'aaaabb111222' => 'ab12'

"aaaabb111222".replace(/(.)\1+/g, "$1");

异步加法

假设有一台本地机器,无法做加减乘除运算(包括位运算),因此无法执行 a + b、a+ = 1 这样的 JS 代码,然后我们提供一个服务器端的 HTTP API,可以传两个数字类型的参数,响应结果是这两个参数的和,这个 HTTP API 的 JS SDK(在本地机器上运行)的使用方法如下:

// 异步加法
function asyncAdd(a, b) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(a + b);
    }, Math.floor(Math.random() * 1000));
  });
}

// 要求 sum 能在最短的时间里返回以上结果
async function sum() {
  console.time("sum");
  function queue(nums) {
    let len = nums.length;
    // 边界条件
    if (len === 0) return Promise.resolve(0);
    if (len === 1) return Promise.resolve(nums[0]);

    const result = [];
    let executing = [];

    while (len >= 2) {
      result.push([nums[len - 1], nums[len - 2]]);
      len = len - 2;
    }

    if (len === 1) {
      executing.push(Promise.resolve(nums[0]));
    }
    executing = [...executing, ...result.map((item) => asyncAdd(...item))];
    return Promise.all(executing).then((data) => queue(data));
  }
  const result = await queue([...arguments]);
  console.timeEnd("sum");
  console.log(result);
  return result;
}

async function sum1(...nums) {
  console.time("sum1");
  // 主运行队列
  let executing = [];
  // 副运行队列
  let executingCopy = [];

  function queue() {
    let len = nums.length;
    if (len === 0) return Promise.resolve(0);
    if (len === 1) return Promise.resolve(nums[0]);

    const result = [];
    while (len >= 2) {
      result.push([nums[len - 1], nums[len - 2]]);
      len = len - 2;
    }

    if (len === 1) {
      executingCopy.push(nums[0]);
    }

    executing = result.map((item) => {
      const p = asyncAdd(...item).then((data) => {
        executing.splice(executing.indexOf(p), 1);
        return data;
      });
      return p;
    });

    function loop() {
      return Promise.race(executing).then((data) => {
        executingCopy.push(data);
        // 副队列满 2 个,创建一个新的 promise 加入主队列
        if (executingCopy.length >= 2) {
          const a = executingCopy.shift();
          const b = executingCopy.shift();
          const p = asyncAdd(a, b).then((data) => {
            executing.splice(executing.indexOf(p), 1);
            return data;
          });
          executing.push(p);
        }
        // 退出条件:主队列执行完毕,副队列只剩最后一个结果
        if (!executing.length) {
          return executingCopy[0];
        }
        return loop();
      });
    }

    return loop();
  }

  const result = await queue([...arguments]);
  console.timeEnd("sum1");
  console.log(result);
  return result;
}

(async () => {
  await sum(1, 4, 6, 9, 2, 4);
  await sum1(1, 4, 6, 9, 2, 4);
  await sum(3, 4, 9, 2, 5, 3, 2, 1, 7);
  await sum1(3, 4, 9, 2, 5, 3, 2, 1, 7);
  await sum(1, 6, 0, 5);
  await sum1(1, 6, 0, 5);
})();

// 思考:
// 1. 并行计算
// 2. 本地缓存
Last Updated:
Contributors: yiliang114